package net.kucoe.grin.context;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

import net.kucoe.grin.Strategy;
import net.kucoe.grin.internal.interaction.exception.*;
import net.kucoe.grin.utils.VariableReader;

import org.objectweb.asm.ClassReader;

/**
 * Describes wired {@link Strategy} method.
 * 
 * @author Vitaliy Basyuk
 */
public class ContextStrategy {
    
    private final Method method;
    private final Object invocationObject;
    private Map<String, Class<?>> parameters;
    
    /**
     * Constructs ContextStrategy.
     * 
     * @param method
     * @param invocationObject
     */
    public ContextStrategy(final Method method, final Object invocationObject) {
        this.method = method;
        this.invocationObject = invocationObject;
    }
    
    /**
     * Returns the method Method.
     * 
     * @return the method Method.
     */
    public Method getMethod() {
        return method;
    }
    
    /**
     * Invokes {@link Strategy} method.
     * 
     * @param params
     * @return method result
     * @throws InteractionException
     */
    public Object invoke(final Map<String, Object> params) throws InteractionException {
        try {
            if (parameters == null) {
                parameters = getParameters(method);
            }
            for (String name : parameters.keySet()) {
                if (!params.containsKey(name)) {
                    throw new ParameterMissingException(name);
                }
                Object value = params.get(name);
                Class<?> type = parameters.get(name);
                if (!type.isInstance(value)) {
                    String valueType = value == null ? "null" : value.getClass().getName();
                    IllegalArgumentException reason =
                            new IllegalArgumentException("Parameter '" + name + "' should be of type '"
                                    + type.getName() + " got '" + valueType + "'");
                    throw new InvocationFailedException(name, reason);
                }
            }
            return method.invoke(invocationObject, params.values().toArray());
        } catch (IllegalArgumentException e) {
            throwUnexpected(e);
        } catch (IllegalAccessException e) {
            throwUnexpected(e);
        } catch (InvocationTargetException e) {
            throwUnexpected(e);
        } catch (IOException e) {
            throwUnexpected(e);
        }
        return null;
    }
    
    private UnexpectedInteractionException throwUnexpected(final Exception e) {
        throw new UnexpectedInteractionException("Error when invoking method: '" + method.getName() + "'", e);
    }
    
    private Map<String, Class<?>> getParameters(final Method m) throws IOException {
        Class<?> declaringClass = m.getDeclaringClass();
        String resourceName = "/" + declaringClass.getName().replace('.', '/') + ".class";
        InputStream classData = declaringClass.getResourceAsStream(resourceName);
        
        VariableReader variableDiscoverer = new VariableReader();
        
        ClassReader r = new ClassReader(classData);
        r.accept(variableDiscoverer, 0);
        
        List<String> variableNames = variableDiscoverer.getVariableNames(m);
        Class<?>[] parameterTypes = m.getParameterTypes();
        Map<String, Class<?>> parameters = new HashMap<String, Class<?>>();
        if (variableNames != null) {
            for (int i = 0; i < parameterTypes.length; i++) {
                parameters.put(variableNames.get(i), parameterTypes[i]);
            }
        }
        return parameters;
    }
}
